import java.net.*;
import java.io.*;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

/*
 * UDP server listens for a request 
 * and sends it back to the client
 */
public class UDPServer{
    //list of clients currently monitored
	private static ArrayList<Client> clientList = new ArrayList<Client>(); 
	private static boolean error = false;
	private static String errorMsg = ""; //Update only if there is an error
	private static Operations operation = new Operations();
	private static byte[] message;
	private static int serverPort = 2222;
	private static DatagramSocket aSocket = null;

	public static void main(String args[]){
		int semanticOption = 0;
		if (args.length > 0) {
			try {
				//Get Semantic option from user
				semanticOption = Integer.parseInt(args[0]);
				switch(semanticOption) {
					case 1:
						System.out.println("\nAt Least Once Semantic");
						break;
					case 2:
						System.out.println("\nAt Most Once Semantic");
						break;
					default:
						System.err.println("\nNo such option exist.");
						System.exit(1);
				}
			} catch (NumberFormatException e) {
				System.err.println("Argument must be an integer");
				System.exit(1);
			}
		} else {
			System.err.println("Please insert semantic option");
			System.exit(1);
		}
			
		System.out.println("Server started");
		Data data;
		Data replyData = null;
		Data prevData = null; //save last request received
		byte[] prevMessage = null; //save last reply sent
		byte[] buffer = new byte[30000];
		try{
			aSocket = new DatagramSocket(serverPort); //bound to host and port
			aSocket.setReuseAddress(true);
			while(true){
				DatagramPacket request = new DatagramPacket(buffer, buffer.length);
				aSocket.receive(request);//blocked if no input
				System.out.println("DEBUG: Request from Client: " + new String(request.getData()));
				//Unmarshall & assemble data
				data = new Data(new String(request.getData()));		
				
				/*Resend reply for duplicate requests
				for at most once semantic*/
				if(semanticOption == 2 && prevData != null && data.getMID() == prevData.getMID()) {
					DatagramPacket reply = new DatagramPacket(
						prevMessage,prevMessage.length,request.getAddress(),request.getPort());
					aSocket.send(reply);
					buffer = new byte[30000];
				} else {
					//Call appropriate methods to handle request
					if(data.getOperation() == data.READ_OP) {
						replyData = readFile(data);
					} else if(data.getOperation() == data.WRITE_OP) {
						replyData = writeFile(data);
					} else if(data.getOperation() == data.MONITOR_OP) {
						replyData = registerClient(data,request.getAddress(),request.getPort());
					} else if(data.getOperation() == data.CREATE_OP) {
						replyData = createFile(data);
					} else if(data.getOperation() == data.APPEND_OP) {
						replyData = appendFile(data);
					} else if(data.getOperation() == data.VALIDATE_OP) {
						replyData = validate(data);
					} else if(data.getOperation() == data.CLOSE_OP) {
						replyData = new Data(data.getMID(),data.REPLY_MSG,data.CLOSE_OP);
					} else {
						replyData = new Data(data.getMID(),data.REPLY_MSG,data.ERROR_OP);
						replyData.setErrorMsg("Operation does not exist.");
					}
					
					aSocket.send(sendData(replyData,request.getAddress(),request.getPort()));
					
					//Update monitored clients when changes are made to file
					if(replyData.getOperation() == replyData.WRITE_OP
						|| replyData.getOperation() == replyData.APPEND_OP) {
						ArrayList<Client> list = updateMonitoredClients(data.getFilename());
						if(list.size() != 0 || !list.isEmpty()) {
							Data dataToSend;
							Client client;
							for(int i = 0; i < list.size(); i++) {
								dataToSend = new Data(data.getMID(),data.REPLY_MSG,data.UPDATE_OP);
								client = list.get(i);
								aSocket.send(sendData(dataToSend,client.address,client.portNum));
							}
						}
					} else if(replyData.getOperation() == replyData.CLOSE_OP) {
						//Close the Server after replying to client
						break;
					}
					
					if(semanticOption == 2) {
						prevData = data;
						prevMessage = message;
					}
					
					//Resets
					data = null;
					replyData = null;
					errorMsg = "";
					error = false;
					buffer = new byte[30000]; //Clear the buffer
				}
			}
		} catch(SocketException e) {
			System.err.println("A SocketException error occured");
		} catch(UnknownHostException e) {
			System.err.println("An UnknownHostException error occured");
		} catch(IOException e) {
			System.err.println("An IOException error occured");
		} finally {
			if (aSocket!=null) {
				aSocket.close();
			}
		}
	}
	
	private static DatagramPacket sendData(Data replyData,InetAddress address,int portNum) {
		System.out.println("DEBUG: replyData="+replyData.getMessageStr());			
		//Marshall data
		message = replyData.getMessageStr().getBytes();//buffer for sending
		
		//To reply, send back to the client's port and address
		DatagramPacket reply = new DatagramPacket(
			message,message.length,address,portNum);
		System.out.println("DEBUG: Send Reply to Client: " + new String(reply.getData()));
		return reply;
	}
	
   /**
	* Register client to be monitored to server
	*/
	private static Data registerClient(Data data,InetAddress address,int portNum) {
		if(operation.fileExists(data.getFilename())) {
			Client client = new Client(data.getMID(),data.getFilename(),address,portNum);
			clientList.add(client);
			Data replyData = new Data(data.getMID(),data.REPLY_MSG,data.getOperation());
			MyThread thread = new MyThread();
			thread.seconds = data.getInterval();
			thread.client = client;
			thread.start();
			return replyData;
		}
		return handleError(data.getMID(),operation.getResult());
	}
	
   /**
	* Check for clients that requires update notifications
	* @return list	list of clients needed to receive notification
	*/
	private static ArrayList<Client> updateMonitoredClients(String filename) {
		ArrayList<Client> list = new  ArrayList<Client>();		
		if(!clientList.isEmpty()) {
			Client client;
			for(int i = 0; i < clientList.size(); i++) {
				client = clientList.get(i);
				if (client.filename.equals(filename)) {
					list.add(client);
				}
			}
		}
		return list;
	}
	
   /**
	* Method is called by thread when time interval is up
	* It removes the relevant client from monitored list
	* and then sends to the Client an END_MONITOR_OP
	*/
	private static void deregisterClient(Client theclient) {
		Data replyData = null;
		if(!clientList.isEmpty()) {
			Client client;
			for(int i = 0; i < clientList.size(); i++) {
				client = clientList.get(i);
				if(client.address == theclient.address) {
					clientList.remove(client);
					replyData = new Data(client.mID,Data.REPLY_MSG,Data.END_MONITOR_OP);
				}
			}
			try {
				aSocket.send(sendData(replyData,theclient.address,theclient.portNum));
			} catch(SocketException e) {
				System.err.println("A SocketException error occured");
				if (aSocket!=null) {
					aSocket.close();
				}
			} catch(UnknownHostException e) {
				System.err.println("An UnknownHostException error occured");
				if (aSocket!=null) {
					aSocket.close();
				}
			} catch(IOException e) {
				System.err.println("An IOException error occured");
				if (aSocket!=null) {
					aSocket.close();
				}
			}
		} else {
			System.err.println("An error occured.");
		}
	}
	
	private static Data writeFile(Data data) {
		if(!operation.randomWrite(data.getFilename(),data.getOffset(),data.getFileContents())) {
			return handleError(data.getMID(),operation.getResult());
		}
		return readFile(data);
	}
	
	private static Data readFile(Data data) {
		//System.out.println("DEBUG: filename="+data.getFilename());
		if(!operation.readFile(data.getFilename())) {
			return handleError(data.getMID(),operation.getResult());
		}
		Data replyData = new Data(data.getMID(),data.REPLY_MSG,data.getOperation());
		replyData.setTMServer(operation.getTM());
		replyData.setFileContents(operation.getContent());
		//System.out.println("DEBUG: Read result="+operation.getContent()+"END");
		return replyData;
	}
	
	private static Data createFile(Data data) {
		if(!operation.createFile(data.getFilename())) {
			return handleError(data.getMID(),operation.getResult());
		}
		Data replyData = new Data(data.getMID(),data.REPLY_MSG,data.CREATE_OP);
		replyData.setTMServer(operation.getTM());
		return replyData;
	}
	
	private static Data appendFile(Data data) {
		if(!operation.appendWrite(data.getFilename(),data.getFileContents())) {
			return handleError(data.getMID(),operation.getResult());
		}
		return readFile(data);
	}
	
	private static Data validate(Data data) {
		if(!operation.getTM(data.getFilename())) {
			return handleError(data.getMID(),operation.getResult());
		}
		Data replyData = new Data(data.getMID(),data.REPLY_MSG,data.VALIDATE_OP);
		replyData.setTMServer(operation.getTM());
		return replyData;
	}
	
	private static Data handleError(int mid,String errorMsg) {
		error = true;
		//System.out.println("DEBUG: errorMsg="+errorMsg);
		Data replyData = new Data(mid,Data.REPLY_MSG,Data.ERROR_OP);
		replyData.setErrorMsg(errorMsg);
		return replyData;
	}
	
	static class MyThread extends Thread
	{
		Timer timer;
		int seconds;
		Client client;

		public void run ()
		{
			System.out.println("DEBUG: Client="+client.address.toString());
			System.out.println("DEBUG: Seconds="+seconds);
			timer = new Timer();
			RemindTask task = new RemindTask();
			task.client = this.client;
			timer.schedule(task, seconds * 1000);
			System.out.println("DEBUG: Task scheduled.");
		}

		class RemindTask extends TimerTask {
			Client client;
			public void run() {
				//System.out.println("DEBUG: Client "+client.address.toString()+" Time's up!");
				System.out.println("DEBUG: Time's up!");
				timer.cancel();
				deregisterClient(client);
			}
		}
	}
	
}

class Client {
	int mID;
	String filename;
	InetAddress address;
	int portNum;
	
	Client(int mID,String filename,InetAddress address,int portNum) {
		this.mID = mID;
		this.filename = filename;
		this.address = address;
		this.portNum = portNum;
	}
}